/*
 * Copyright (c) 2022. China Mobile (SuZhou) Software Technology Co.,Ltd. All rights reserved.
 * Lakehouse is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */

package com.chinamobile.cmss.lakehouse.api.controller;

import static com.chinamobile.cmss.lakehouse.common.enums.Status.METADATA_QUERY_LIST_PAGING_ERROR;
import static com.chinamobile.cmss.lakehouse.common.enums.Status.METADATA_USER_DBS_ERROR;

import com.chinamobile.cmss.lakehouse.api.exceptions.ApiException;
import com.chinamobile.cmss.lakehouse.api.service.MetaDataService;
import com.chinamobile.cmss.lakehouse.common.Constants;
import com.chinamobile.cmss.lakehouse.common.annotation.AccessLogAnnotation;
import com.chinamobile.cmss.lakehouse.common.dto.MetaDataNodeDto;
import com.chinamobile.cmss.lakehouse.common.dto.SearchMetadataTableDto;
import com.chinamobile.cmss.lakehouse.common.dto.SqlExecuteContextDto;
import com.chinamobile.cmss.lakehouse.common.dto.TableNameDto;
import com.chinamobile.cmss.lakehouse.common.enums.Status;
import com.chinamobile.cmss.lakehouse.common.utils.Result;
import com.chinamobile.cmss.lakehouse.dao.entity.UserEntity;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;

import javax.validation.Valid;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.annotations.ApiIgnore;

@Slf4j
@Api(value = "SQL Query Console", protocols = "http")
@RestController
@RequestMapping(value = "/metadata")
public class MetadataController extends BaseController {

    @Autowired
    private MetaDataService metaDataService;

    /**
     * search metadata by conditional
     *
     * @param loginUser
     * @param searchMetadataTableDto
     * @return
     */
    @ApiOperation(value = "queryUserList", notes = "QUERY_USER_LIST_NOTES")
    @ApiImplicitParams({})
    @RequestMapping(value = "/user/tables/search", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.OK)
    @ApiException(METADATA_QUERY_LIST_PAGING_ERROR)
    @AccessLogAnnotation(ignoreRequestArgs = "loginUser")
    public Result queryUserList(@ApiIgnore @RequestAttribute(value = Constants.SESSION_USER) UserEntity loginUser,
                                @Valid @RequestBody SearchMetadataTableDto searchMetadataTableDto) {
        Map<String, Object> result = metaDataService.searchUserTable(loginUser, searchMetadataTableDto);
        return convertToResult(result);
    }

    /**
     * sqlExecuteProxy
     *
     * @param loginUser            login user
     * @param sqlExecuteContextDto SqlExecuteContextDto
     * @return Result
     */
    @RequestMapping(value = "/user/sqlExecuteProxy", method = RequestMethod.POST)
    @ApiOperation(value = "user sqlExecuteProxy", notes = "sqlExecuteProxy")
    @ApiImplicitParams({})
    @ResponseStatus(HttpStatus.OK)
    @ApiException(METADATA_USER_DBS_ERROR)
    public Result sqlExecuteProxy(@ApiIgnore @RequestAttribute(value = Constants.SESSION_USER) UserEntity loginUser,
                                  @Valid @RequestBody SqlExecuteContextDto sqlExecuteContextDto) {
        Map<String, Object> result = metaDataService.sqlExecuteProxy(loginUser, sqlExecuteContextDto);
        return convertToResult(result);
    }

    /**
     * get user databases
     *
     * @param loginUser login user
     * @return Result
     */
    @RequestMapping(value = "/user/databases", method = RequestMethod.GET)
    @ApiOperation(value = "user databases", notes = "get user databases")
    @ApiImplicitParams({})
    @ResponseStatus(HttpStatus.OK)
    @ApiException(METADATA_USER_DBS_ERROR)
    public Result getUserDatabases(@ApiIgnore @RequestAttribute(value = Constants.SESSION_USER) UserEntity loginUser) {
        Map<String, Object> result = metaDataService.getUserDatabases(loginUser);
        return convertToResult(result);
    }

    @RequestMapping(value = "/databases/{databaseName}/tables/{tableName}", method = RequestMethod.DELETE)
    @ApiOperation(value = "delete table")
    @ApiImplicitParams({})
    @ResponseStatus(HttpStatus.OK)
    @ApiException(METADATA_USER_DBS_ERROR)
    public Result deleteTable(@ApiIgnore @RequestAttribute(value = Constants.SESSION_USER) UserEntity loginUser,
                              @ApiParam("db name") @PathVariable String databaseName,
                              @ApiParam("table name") @PathVariable String tableName) {
        Map<String, Object> result = new HashMap<>();
        metaDataService.deleteTable(loginUser, TableNameDto.builder()
            .databaseName(databaseName)
            .tableName(tableName)
            .build());
        result.put(Constants.STATUS, HttpStatus.OK);
        return convertToResult(result);
    }

    @RequestMapping(value = "/user/tables/batch/deleteTable", method = RequestMethod.POST)
    @ApiOperation(value = "batch delete table")
    @ApiImplicitParams({})
    @ResponseStatus(HttpStatus.OK)
    @ApiException(METADATA_USER_DBS_ERROR)
    public Result batchDeleteTable(@ApiIgnore @RequestAttribute(value = Constants.SESSION_USER) UserEntity loginUser,
                                   @Valid @RequestBody TableNameDto[] deleteTables) {
        List<String> failureTables = new ArrayList<>();
        Map<String, Object> result = new HashMap<>();
        for (TableNameDto deleteTable : deleteTables) {
            try {
                deleteTable(loginUser, deleteTable.getDatabaseName(), deleteTable.getTableName());
            } catch (Exception e) {
                failureTables.add(deleteTable.toString());
            }
        }
        if (failureTables.size() == 0) {
            result.put(Constants.MESSAGE, "delete success");
            result.put(Constants.STATUS, HttpStatus.OK);
        } else {
            result.put(Constants.MESSAGE, failureTables.stream().collect(Collectors.joining(",", "[", "]")) + " delete failed");
            result.put(Constants.STATUS, HttpStatus.INTERNAL_SERVER_ERROR);
        }
        return convertToResult(result);
    }

    @RequestMapping(value = "/tree", method = RequestMethod.GET)
    @ApiOperation(value = "get metadata tree")
    @ApiImplicitParams({})
    @ResponseStatus(HttpStatus.OK)
    @ApiException(METADATA_USER_DBS_ERROR)
    public Result metadataTree(@ApiIgnore @RequestAttribute(value = Constants.SESSION_USER) UserEntity loginUser) {
        Result result = new Result();
        Map<String, List<MetaDataNodeDto>> tree = metaDataService.metadataTree(loginUser);
        result.setCode(Status.SUCCESS.getCode());
        result.setData(tree);
        return result;
    }

    @RequestMapping(value = "/database/tree", method = RequestMethod.GET)
    @ApiOperation(value = "get metadata database tree, not return back table and column info")
    @ApiImplicitParams({})
    @ResponseStatus(HttpStatus.OK)
    @ApiException(METADATA_USER_DBS_ERROR)
    public Result metadataDatabaseTree(@ApiIgnore @RequestAttribute(value = Constants.SESSION_USER) UserEntity loginUser) {
        Result tree = metaDataService.metadataDatabaseTree(loginUser);
        return tree;
    }

    @RequestMapping(value = "/database/table", method = RequestMethod.GET)
    @ApiOperation(value = "get metadata database's table tree, not return back column info")
    @ApiImplicitParams({})
    @ResponseStatus(HttpStatus.OK)
    @ApiException(METADATA_USER_DBS_ERROR)
    public Result metadataTableTree(@ApiIgnore @RequestAttribute(value = Constants.SESSION_USER) UserEntity loginUser,
                                    @RequestParam("databaseName") String databaseName) {
        Result tree = metaDataService.getTables(loginUser, databaseName);
        return tree;
    }

    @RequestMapping(value = "/database/table/column", method = RequestMethod.GET)
    @ApiOperation(value = "get metadata database table's column tree")
    @ApiImplicitParams({})
    @ResponseStatus(HttpStatus.OK)
    @ApiException(METADATA_USER_DBS_ERROR)
    public Result metadataColumnTree(@ApiIgnore @RequestAttribute(value = Constants.SESSION_USER) UserEntity loginUser,
                                     @RequestParam("databaseName") String databaseName,
                                     @RequestParam("tableName") String tableName) {
        Result tree = metaDataService.getColumns(loginUser, databaseName, tableName);
        return tree;
    }

    @RequestMapping(value = "/mock/tree", method = RequestMethod.GET)
    @ApiOperation(value = "get metadata tree")
    @ApiImplicitParams({})
    @ResponseStatus(HttpStatus.OK)
    @ApiException(METADATA_USER_DBS_ERROR)
    public List<MetaDataNodeDto> mockMetaDataTree() {
        final int treeSize = 50;
        List<MetaDataNodeDto> databases = new ArrayList<>(treeSize);
        for (int i = 0; i < treeSize; i++) {
            String databaseName = "test" + i;
            final MetaDataNodeDto db = createDatabases(databaseName);
            String[] tableNames = new String[20];
            for (int j = 0; j < tableNames.length; j++) {
                tableNames[j] = "table" + i + j;
            }
            db.setChildren(createTables(tableNames));
            databases.add(db);
        }
        return databases;
    }

    private final Random random = new Random();

    private MetaDataNodeDto createDatabases(String dataBaseName) {
        MetaDataNodeDto metaDataNodeDto = new MetaDataNodeDto();
        metaDataNodeDto.setLabel(dataBaseName);
        MetaDataNodeDto.ScopedSlots scopedSlots = new MetaDataNodeDto.ScopedSlots();
        metaDataNodeDto.setKey(random.nextInt(Integer.MAX_VALUE) + "");
        scopedSlots.setTitle(MetaDataNodeDto.TitleType.database);
        metaDataNodeDto.setScopedSlots(scopedSlots);
        return metaDataNodeDto;
    }

    private List<MetaDataNodeDto> createTables(String... tables) {
        List<MetaDataNodeDto> t = new ArrayList<>(tables.length);
        for (String table : tables) {
            MetaDataNodeDto tmp = new MetaDataNodeDto();
            tmp.setLabel(table);
            MetaDataNodeDto.ScopedSlots scopedSlots = new MetaDataNodeDto.ScopedSlots();
            scopedSlots.setTitle(MetaDataNodeDto.TitleType.table);
            tmp.setKey(random.nextInt(Integer.MAX_VALUE) + "");
            tmp.setScopedSlots(scopedSlots);
            tmp.setChildren(createColumn("id", "name", "age", "location", "phone", "email", "prop1", "prop2", "createTime", "creator"));
            t.add(tmp);
        }
        return t;
    }

    private List<MetaDataNodeDto> createColumn(String... colNames) {
        List<MetaDataNodeDto> metaDataNodeDtos = new ArrayList<>();
        for (String colName : colNames) {
            MetaDataNodeDto tmp = new MetaDataNodeDto();
            tmp.setKey(random.nextInt(Integer.MAX_VALUE) + "");
            tmp.setLabel(colName);
            MetaDataNodeDto.ScopedSlots scopedSlots = new MetaDataNodeDto.ScopedSlots();
            scopedSlots.setTitle(MetaDataNodeDto.TitleType.column);
            tmp.setScopedSlots(scopedSlots);
            metaDataNodeDtos.add(tmp);
        }
        return metaDataNodeDtos;
    }

}
